[数据结构 - 第6章] 树之二叉排序树(C语言实现)

一、什么是二叉排序树?

对于普通的顺序存储来说,插入、删除操作很简便,效率高;而这样的表由于无序造成查找的效率很低

对于有序线性表来说(顺序存储的),查找可用折半、插值、斐波那契等查找算法实现,效率高;而因为要保持有序,在插入和删除时不得不耗费大量的时间

那么,如何既使得插入和删除效率不错,又可以比较高效率地实现查找的算法呢?


先看一个例子:

现在我们的目标是插入和查找同样高效。假设我们的数据集开始只有一个数 {62},然后现在需要将 88 插入数据集,于是数据集成了 {62,88},还保持着从小到大有序。再查找有没有 58,没有则插入,可此时要想在线性表的顺序存储中有序,就得移动 62 和 88 的位置,如下左图,可不可以不移动呢?嗯,当然是可以,那就是二叉树结构。当我们用二叉树的方式时,首先我们将第一个数 62 定为根结点,88 因为比 62 大,因此让它做 62 的右子树,58 因比 62 小,所以成为它的左子树。此时 58 的插入并没有影响到 62 与 88 的关系,如下右图所示。


也就是说,若我们现在需要对集合 {62,88,58,47,35,73,51,99,37,93} 做查找,在我们打算创建此集合时就考虑用二叉树结构,而且是排好序的二叉树来创建。如下图所示,62、88、58 创建好后,下一个数 47 因比 58 小,是它的左子树(见③),35 是 47 的左子树(见④),73 比 62 大,但却比 88 小,是 88 的左子树(见⑤),51 比 62 小、比 58 小、比 47 大,是 47 的右子树(见⑥),99 比 62、88 都大,是 88 的右子树(见⑦),37 比 62、58、47 都小,但却比 35 大,是 35 的右子树(见⑧),93 则因比 62、88 大是 99 的左子树(见⑨)。


这样我们就得到了一棵二叉树,并且当我们对它进行中序遍历时,就可以得到一个有序的序列 {35,37,47,51,58,62,73,88,93,99},所以我们通常称它为二叉排序树。

二叉排序树(Binary Sort Tree),又称为二叉查找树。它或者是一棵空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值
  • 它的左、右子树也分别为二叉排序树

构造一棵二叉排序树的目的,其实并不是为了排序,而是为了提高查找和插入删除关键字的速度。不管怎么说,在一个有序数据集上的查找,速度总是要快于无序的数据集的,而二叉排序树这种非线性的结构,也有利于插入和删除的实现。


二、基本操作

首先提供一个二叉树的结构:

/* 二叉树的链表结点结构定义 */
typedef  struct BiTNode	/* 结点结构 */
{
	int data;	// 结点数据
	struct BiTNode *lchild, *rchild;	// 左右孩子指针
} BiTNode, BiTree;

2.1 二叉排序树查找操作

思路:当二叉树非空时,首先将待查找的键值与根节点的键值比较,若小于根节点的键值,则继续查找左子树,否则查找右子树。重复上述过程,直至查找成功返回 TRUE,否则返回 FALSE。

// 递归查找二叉排序树T中是否存在key,指针f指向T的双亲,其初始调用值为NULL
Status searchBST(BiTree *T, int key, BiTree *f, BiTree **p)
{
	if (T == NULL) // 查找不成功,指针p指向查找路径上访问的最后一个结点并返回FALSE
	{
		*p = f;
		return FALSE;
	}
	else if (T->data == key) // 查找成功,则指针p指向该数据元素结点,并返回TRUE 
	{
		*p = T;
		return TRUE;
	}
	
	if (key < T->data)
		return searchBST(T->lchild, key, T, p);  // 在左子树中继续查找(注意此时f的赋值)
	else
		return searchBST(T->rchild, key, T, p);  // 在右子树中继续查找
}

2.2 二叉排序树插入操作

思路:与查找类似,但需要一个父节点来进行赋值。代码如下:

//  当二叉排序树T中不存在关键字等于key的数据元素时,插入key并返回TRUE,否则返回FALSE
Status insertBST(BiTree **T, int key)
{
	BiTree *p; // 指针p指向查找路径上访问的最后一个结点

	// 不存在关键字等于key的数据元素时,才插入
	if (!searchBST(*T, key, NULL, &p)) // 通过指针p获得查找路径上访问的最后一个结点的地址
	{
		BiTNode *temp = (BiTNode *)malloc(sizeof(BiTNode));
		temp->data = key;
		temp->lchild = temp->rchild = NULL;

		// 根据key与最后一个节点key值比较大小,决定是在左子树还是右子树插入
		if (p == NULL)
			*T = temp;	// T为空树,则插入temp为新的根结点
		else if (key < p->data)
			p->lchild = temp;
		else
			p->rchild = temp;

		return TRUE;
	}
	else
		return FALSE;  // 树中已有关键字相同的结点,不再插入
}

2.3 二叉排序树删除元素操作

对于二叉排序树的删除要注意,我们不能因为删除了结点,而让这棵树变得不满足二叉排序树的特性,所以删除需要考虑多种情况。

(1)删除叶子结点

  直接删除,不影响原树,如下图所示:


(2)删除仅有左或右子树的结点

  节点删除后,将它的左子树或右子树整个移动到删除节点的位置就可以,子承父业,如下图所示:


(3)删除左右子树都有的结点

我们仔细观察一下,47 的两个子树中能否找出一个结点可以代替 47 呢?果然有,37 或者 48 都可以代替 47,此时在删除 47后,整个二叉排序树并没有发生什么本质的改变。

为什么是 37 和 48?对的,它们正好是二叉排序树中比它小或比它大的最接近 47 的两个数。也就是说,如果我们对这棵二叉排序树进行中序遍历,得到的序列 {29, 35, 36, 37, 47, 48, 49, 50, 51, 56, 58, 62, 73, 88, 93, 99}。

因此,比较好的办法就是,找到删除结点 p 的中序序列的直接前驱(或直接后驱)s,用 s 来替换结点 p,然后删除结点 s,如下图所示:


根据我们对删除结点三种情况的分析:

  1. 叶子结点;
  2. 仅有左或右子树的结点;
  3. 左右子树都有的结点。

下面是 deleteNode 的代码:

// 从二叉排序树中删除结点p,并重接它的左或右子树。
Status deleteNode(BiTree **p)
{
	BiTree *q, *temp;
	if ((*p)->rchild == NULL) // 右子树空则只需重接它的左子树(待删结点是叶子也走此分支)
	{
		q = *p; *p = (*p)->lchild; free(q);
	}
	else if ((*p)->lchild == NULL) // 只需重接它的右子树
	{
		q = *p; *p = (*p)->rchild; free(q);
	}
	else /* 左右子树均不空 */
	{
		q = *p; 
		temp = (*p)->lchild;

		while (temp->rchild) // 转左,然后向右到尽头(找待删结点的前驱)
		{
			q = temp;
			temp = temp->rchild;
		}

		(*p)->data = temp->data; // temp指向被删结点的直接前驱(将被删结点前驱的值取代被删结点的值)
		if (q != *p)
			q->rchild = temp->lchild; // 重接q的右子树
		else
			q->lchild = temp->lchild; // 重接q的左子树

		free(temp);
	}

	return TRUE;
}

2.4 二叉排序树删除结点操作

下面这个算法是递归方式对二叉排序树 T 查找 key,查找到时删除。

// 若二叉排序树T中存在关键字等于key的数据元素时,则删除该数据元素结点
Status deleteBST(BiTree **T, int key)
{
	if (!*T) // 不存在关键字等于key的数据元素
		return FALSE;
	else
	{
		if (key == (*T)->data) // 找到关键字等于key的数据元素
			return deleteNode(T);
		else if (key < (*T)->data)
			return deleteBST(&(*T)->lchild, key);
		else
			return deleteBST(&(*T)->rchild, key);

	}
}

可以看出,这段代码和前面的二叉排序树查找几乎完全相同,唯一区别在于当找到对应 key 值的结点时,执行的是删除操作。


三、主函数执行

int main(void)
{
	int a[10] = { 62, 88, 58, 47, 35, 73, 51, 99, 37, 93 };
	BiTree *T = NULL;

	// 插入元素
	for (int i = 0; i<10; i++)
	{
		insertBST(&T, a[i]);
	}

	// 删除元素
	deleteBST(&T, 93);
	deleteBST(&T, 47);

	// 中序递归遍历
	printf("中序递归遍历: ");
	inOrderTraverse(T); // 中序递归遍历,会得到一个有序的序列

	printf("\n\n另外,本样例建议断点跟踪查看二叉排序树结构\n\n");

	return 0;
}

输出结果如下图所示:


四、二叉排序树总结

总之,二叉排序树是以链接的方式存储,保持了链接存储结构在执行插入或删除操作时不用移动元素的优点,只要找到合适的插入和删除位置后,仅需修改链接指针即可。

而对于二叉排序树的查找,走的就是从根结点到要查找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数。技术情况,最少为1次,即根结点就是要找的结点;最多也不会超过树的深度。也就是说,二叉排序树的查找性能取决于二叉排序树的形状。可问题是,二叉排序树的形状是不确定的

例如 {62, 88, 58, 47, 35, 73, 51, 99, 37, 93} 这样的数组,我们可以构建如下左图的二叉排序树。但如果数组元素的次序是从小到大有序,如 {35, 37, 47, 51, 58, 62, 73, 88, 93, 99},则二叉排序树就成了极端的右斜树,注意它依然是一棵二叉排序树,如下右图。此时,同样是查找结点 99,左图只需要两次比较,而右图就需要 10 次比较才可以得到结果,二者差异很大。


也就是说,我们希望二叉排序树是比较平衡的,即其深度与完全二叉树相同,那么查找的时间复杂也就为 O(logn),近似于折半查找。而像上图右边这种情况,查找时间复杂度为 O(n),这等同于顺序查找。因此,如果我们希望对一个集合按二叉排序树查找,最好是把它构建成一棵平衡的二叉排序树


参考:

《大话数据结构 - 第8章》 查找

二叉排序树


posted @ 2019-01-30 18:57  fengMisaka  阅读(1923)  评论(0编辑  收藏  举报